"""MQTT protocol connector for IoT devices

Supports MQTT 3.1.1 and 5.0 with QoS 0/1/2
Thread-safe async implementation with auto-reconnect
"""

from __future__ import annotations

from typing import Any, Optional
import asyncio
import json
import logging
from datetime import datetime

from aiomqtt import Client, MqttError
from aiomqtt.types import PayloadType

from app.connectors.base import BaseConnector, ConnectionStatus, DataPoint, DataQuality

logger = logging.getLogger(__name__)


class MQTTConnector(BaseConnector):
    """MQTT protocol connector for IoT devices

    Supports MQTT 3.1.1 and 5.0 with QoS 0/1/2, TLS/SSL, auto-reconnect

    Configuration:
        - broker: MQTT broker hostname (required)
        - port: MQTT broker port (default: 1883, TLS: 8883)
        - qos: Quality of Service 0/1/2 (default: 0)
        - client_id: Client identifier (default: auto-generated)
        - use_tls: Enable TLS/SSL (default: False)
        - username: Authentication username (optional)
        - password: Authentication password (optional)
        - keepalive: Keep-alive interval in seconds (default: 60)
        - reconnect_interval: Reconnect delay in seconds (default: 5)
        - timeout: Operation timeout in seconds (default: 5)

    Example:
        config = {
            "broker": "mqtt.example.com",
            "port": 1883,
            "qos": 1,
            "username": "user",
            "password": "pass"
        }
        connector = MQTTConnector(config)
        await connector.connect()
        data = await connector.read_single("sensor/temperature")
    """

    def __init__(self, config: dict[str, Any]) -> None:
        """Initialize MQTT connector with configuration

        Args:
            config: MQTT connection configuration dictionary
        """
        super().__init__(config)
        self.client: Optional[Client] = None
        self.broker: str = config["broker"]
        self.port: int = config.get("port", 1883)
        self.qos: int = config.get("qos", 0)
        self.client_id: str = config.get("client_id", f"opc_monitor_{id(self)}")
        self.use_tls: bool = config.get("use_tls", False)
        self.username: Optional[str] = config.get("username")
        self.password: Optional[str] = config.get("password")
        self.keepalive: int = config.get("keepalive", 60)
        self.reconnect_interval: int = config.get("reconnect_interval", 5)
        self.timeout: float = config.get("timeout", 5.0)

        # Internal state
        self._reconnect_task: Optional[asyncio.Task] = None
        self._subscriptions: set[str] = set()

    async def connect(self) -> bool:
        """Establish MQTT connection with auto-reconnect

        Returns:
            True if connection successful

        Raises:
            MqttError: If connection fails after retry attempts
        """
        try:
            logger.info(f"Connecting to MQTT broker {self.broker}:{self.port}")

            # Create client with configuration
            self.client = Client(
                hostname=self.broker,
                port=self.port,
                username=self.username,
                password=self.password,
                client_id=self.client_id,
                keepalive=self.keepalive,
                timeout=self.timeout,
                tls_context=None if not self.use_tls else True,  # Use default TLS if enabled
            )

            # Connect with timeout
            await asyncio.wait_for(
                self.client.__aenter__(),
                timeout=self.timeout
            )

            self.status = ConnectionStatus.CONNECTED
            logger.info(f"Connected to MQTT broker {self.broker}:{self.port}")
            return True

        except asyncio.TimeoutError:
            self.status = ConnectionStatus.ERROR
            error_msg = f"Connection timeout to MQTT broker {self.broker}:{self.port}"
            logger.error(error_msg)
            raise MqttError(error_msg)

        except MqttError as e:
            self.status = ConnectionStatus.ERROR
            logger.error(f"MQTT connection error: {e}")
            raise

        except Exception as e:
            self.status = ConnectionStatus.ERROR
            logger.error(f"Unexpected error connecting to MQTT broker: {e}")
            raise MqttError(f"Connection failed: {e}")

    async def disconnect(self) -> bool:
        """Close MQTT connection gracefully

        Returns:
            True if disconnection successful
        """
        if self.client is not None:
            try:
                logger.info(f"Disconnecting from MQTT broker {self.broker}:{self.port}")
                await self.client.__aexit__(None, None, None)
                self.status = ConnectionStatus.DISCONNECTED
                self._subscriptions.clear()
                logger.info("MQTT disconnected successfully")
                return True
            except Exception as e:
                logger.error(f"Error during MQTT disconnect: {e}")
                return False
            finally:
                self.client = None
        return True

    async def read_single(self, address: str) -> DataPoint:
        """Subscribe to MQTT topic and read a single value

        Args:
            address: MQTT topic to subscribe (e.g., "sensor/temperature")

        Returns:
            DataPoint with topic value

        Raises:
            MqttError: If read operation fails
            asyncio.TimeoutError: If no message received within timeout
        """
        if self.client is None or self.status != ConnectionStatus.CONNECTED:
            raise MqttError("MQTT client not connected")

        logger.debug(f"Reading from MQTT topic: {address}")

        try:
            # Subscribe to topic
            await self.client.subscribe(address, qos=self.qos)
            self._subscriptions.add(address)

            # Wait for first message with timeout
            async with asyncio.timeout(self.timeout):
                async for message in self.client.messages:
                    if message.topic.matches(address):
                        value = self._parse_payload(message.payload)
                        logger.debug(f"Read from {address}: {value}")

                        return DataPoint(
                            address=address,
                            value=value,
                            timestamp=datetime.utcnow(),
                            quality=DataQuality.GOOD
                        )

            # Timeout reached without receiving message
            raise asyncio.TimeoutError(f"No message received from topic {address} within {self.timeout}s")

        except asyncio.TimeoutError as e:
            logger.error(f"Timeout reading from MQTT topic {address}: {e}")
            raise

        except MqttError as e:
            logger.error(f"MQTT error reading from {address}: {e}")
            raise

        except Exception as e:
            logger.error(f"Unexpected error reading from MQTT topic {address}: {e}")
            raise MqttError(f"Read failed: {e}")

    async def read_multiple(self, addresses: list[str]) -> list[DataPoint]:
        """Subscribe to multiple MQTT topics and read values

        Args:
            addresses: List of MQTT topics to subscribe

        Returns:
            List of DataPoints with topic values

        Raises:
            MqttError: If read operation fails
        """
        if self.client is None or self.status != ConnectionStatus.CONNECTED:
            raise MqttError("MQTT client not connected")

        logger.debug(f"Reading from {len(addresses)} MQTT topics")

        try:
            # Subscribe to all topics
            for address in addresses:
                await self.client.subscribe(address, qos=self.qos)
                self._subscriptions.add(address)

            # Collect messages from all topics
            data_points: dict[str, DataPoint] = {}

            async with asyncio.timeout(self.timeout):
                async for message in self.client.messages:
                    # Check if this topic is in our request
                    for address in addresses:
                        if message.topic.matches(address):
                            value = self._parse_payload(message.payload)
                            data_points[address] = DataPoint(
                                address=address,
                                value=value,
                                timestamp=datetime.utcnow(),
                                quality=DataQuality.GOOD
                            )
                            break

                    # Exit when all topics received
                    if len(data_points) == len(addresses):
                        break

            # Return results in same order as requested
            results = []
            for address in addresses:
                if address in data_points:
                    results.append(data_points[address])
                else:
                    # Return bad quality for missing topics
                    logger.warning(f"No message received from topic {address}")
                    results.append(DataPoint(
                        address=address,
                        value=None,
                        timestamp=datetime.utcnow(),
                        quality=DataQuality.BAD
                    ))

            logger.debug(f"Read {len(results)} values from MQTT topics")
            return results

        except asyncio.TimeoutError:
            logger.error(f"Timeout reading from MQTT topics")
            # Return partial results with BAD quality for missing
            results = []
            for address in addresses:
                if address in data_points:
                    results.append(data_points[address])
                else:
                    results.append(DataPoint(
                        address=address,
                        value=None,
                        timestamp=datetime.utcnow(),
                        quality=DataQuality.BAD
                    ))
            return results

        except Exception as e:
            logger.error(f"Error reading from MQTT topics: {e}")
            raise MqttError(f"Read multiple failed: {e}")

    async def write_single(self, address: str, value: Any) -> bool:
        """Publish value to MQTT topic

        Args:
            address: MQTT topic to publish to
            value: Value to publish (will be JSON-serialized)

        Returns:
            True if publish succeeded

        Raises:
            MqttError: If write operation fails
        """
        if self.client is None or self.status != ConnectionStatus.CONNECTED:
            raise MqttError("MQTT client not connected")

        logger.debug(f"Writing to MQTT topic {address}: {value}")

        try:
            # Serialize value to JSON
            payload = self._serialize_value(value)

            # Publish message
            await self.client.publish(
                address,
                payload=payload,
                qos=self.qos,
                retain=False
            )

            logger.debug(f"Published to {address}: {value}")
            return True

        except MqttError as e:
            logger.error(f"MQTT error writing to {address}: {e}")
            raise

        except Exception as e:
            logger.error(f"Error writing to MQTT topic {address}: {e}")
            raise MqttError(f"Write failed: {e}")

    async def write_multiple(self, write_requests: list[dict[str, Any]]) -> list[bool]:
        """Publish multiple values to MQTT topics

        Args:
            write_requests: List of dicts with 'address' and 'value' keys

        Returns:
            List of success flags for each write

        Raises:
            MqttError: If write operation fails
        """
        if self.client is None or self.status != ConnectionStatus.CONNECTED:
            raise MqttError("MQTT client not connected")

        logger.debug(f"Writing to {len(write_requests)} MQTT topics")

        results = []
        for request in write_requests:
            try:
                address = request["address"]
                value = request["value"]
                success = await self.write_single(address, value)
                results.append(success)
            except Exception as e:
                logger.error(f"Error writing to {request.get('address', 'unknown')}: {e}")
                results.append(False)

        return results

    def _parse_payload(self, payload: PayloadType) -> Any:
        """Parse MQTT message payload to Python value

        Attempts JSON parsing first, falls back to string conversion

        Args:
            payload: MQTT message payload (bytes or string)

        Returns:
            Parsed value (dict, list, str, int, float, bool, or None)
        """
        if payload is None:
            return None

        # Convert bytes to string
        if isinstance(payload, bytes):
            payload_str = payload.decode('utf-8')
        else:
            payload_str = str(payload)

        # Try JSON parsing first
        try:
            return json.loads(payload_str)
        except (json.JSONDecodeError, ValueError):
            pass

        # Try numeric conversion
        try:
            # Try integer
            if '.' not in payload_str:
                return int(payload_str)
            # Try float
            return float(payload_str)
        except ValueError:
            pass

        # Try boolean
        if payload_str.lower() in ('true', 'false'):
            return payload_str.lower() == 'true'

        # Return as string
        return payload_str

    def _serialize_value(self, value: Any) -> str:
        """Serialize Python value to MQTT payload string

        Args:
            value: Python value to serialize

        Returns:
            JSON string representation
        """
        if value is None:
            return 'null'

        if isinstance(value, (dict, list)):
            return json.dumps(value)

        if isinstance(value, bool):
            return 'true' if value else 'false'

        if isinstance(value, (int, float)):
            return str(value)

        return str(value)

    async def health_check(self) -> bool:
        """Check MQTT connection health

        Returns:
            True if connected and responsive
        """
        if self.client is None or self.status != ConnectionStatus.CONNECTED:
            return False

        try:
            # Try publishing to test topic
            test_topic = f"$SYS/test/{self.client_id}"
            await self.client.publish(test_topic, payload="ping", qos=0)
            return True
        except Exception as e:
            logger.error(f"MQTT health check failed: {e}")
            return False

    def __str__(self) -> str:
        """String representation"""
        return f"MQTTConnector(broker={self.broker}:{self.port}, client_id={self.client_id}, status={self.status})"

    def __repr__(self) -> str:
        """Debug representation"""
        return (
            f"MQTTConnector(broker={self.broker}, port={self.port}, "
            f"qos={self.qos}, use_tls={self.use_tls}, status={self.status})"
        )
